/*
 * Copyright 2015-2025 the original author or authors.
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License v2.0 which
 * accompanies this distribution and is available at
 *
 * https://www.eclipse.org/legal/epl-v20.html
 */

package org.junit.platform.commons.util;

import static org.apiguardian.api.API.Status.INTERNAL;
import static org.junit.platform.commons.util.ReflectionUtils.EMPTY_CLASS_ARRAY;
import static org.junit.platform.commons.util.ReflectionUtils.findMethod;
import static org.junit.platform.commons.util.ReflectionUtils.isStatic;
import static org.junit.platform.commons.util.ReflectionUtils.tryToLoadClass;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.Arrays;

import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.junit.platform.commons.function.Try;

/**
 * Internal Kotlin-specific reflection utilities
 *
 * @since 1.13.3
 */
@API(status = INTERNAL, since = "1.13.3")
public class KotlinReflectionUtils {

	private static final String DEFAULT_IMPLS_CLASS_NAME = "DefaultImpls";

	private static final @Nullable Class<? extends Annotation> kotlinMetadata;
	private static final @Nullable Class<?> kotlinCoroutineContinuation;
	private static final boolean kotlinReflectPresent;
	private static final boolean kotlinxCoroutinesPresent;

	static {
		var metadata = tryToLoadKotlinMetadataClass();
		kotlinMetadata = metadata.toOptional().orElse(null);
		kotlinCoroutineContinuation = metadata //
				.andThen(__ -> tryToLoadClass("kotlin.coroutines.Continuation")) //
				.toOptional() //
				.orElse(null);
		kotlinReflectPresent = metadata.andThen(__ -> tryToLoadClass("kotlin.reflect.jvm.ReflectJvmMapping")) //
				.toOptional() //
				.isPresent();
		kotlinxCoroutinesPresent = metadata.andThen(__ -> tryToLoadClass("kotlinx.coroutines.BuildersKt")) //
				.toOptional() //
				.isPresent();
	}

	@SuppressWarnings("unchecked")
	private static Try<Class<? extends Annotation>> tryToLoadKotlinMetadataClass() {
		return tryToLoadClass("kotlin.Metadata") //
				.andThenTry(it -> (Class<? extends Annotation>) it);
	}

	/**
	 * @since 6.0
	 */
	@API(status = INTERNAL, since = "6.0")
	public static boolean isKotlinSuspendingFunction(Method method) {
		if (!method.isSynthetic() && kotlinCoroutineContinuation != null && isKotlinType(method.getDeclaringClass())) {
			int parameterCount = method.getParameterCount();
			return parameterCount > 0 //
					&& method.getParameterTypes()[parameterCount - 1] == kotlinCoroutineContinuation;
		}
		return false;
	}

	/**
	 * Determines whether the supplied class is a {@code DefaultImpls} class
	 * generated by the Kotlin compiler.
	 *
	 * <p>See
	 * <a href="https://kotlinlang.org/docs/interfaces.html#jvm-default-method-generation-for-interface-functions">Kotlin documentation</a>
	 * for details.
	 */
	public static boolean isKotlinInterfaceDefaultImplsClass(Class<?> clazz) {
		if (!isKotlinType(clazz) || !DEFAULT_IMPLS_CLASS_NAME.equals(clazz.getSimpleName()) || !isStatic(clazz)) {
			return false;
		}

		Class<?> enclosingClass = clazz.getEnclosingClass();
		if (enclosingClass != null && enclosingClass.isInterface()) {
			return Arrays.stream(clazz.getDeclaredMethods()) //
					.anyMatch(method -> isCompilerGeneratedDefaultMethod(method, enclosingClass));
		}

		return false;
	}

	private static boolean isCompilerGeneratedDefaultMethod(Method method, Class<?> enclosingClass) {
		if (isStatic(method) && method.getParameterCount() > 0) {
			var parameterTypes = method.getParameterTypes();
			if (parameterTypes[0] == enclosingClass) {
				var originalParameterTypes = copyWithoutFirst(parameterTypes);
				return findMethod(enclosingClass, method.getName(), originalParameterTypes).isPresent();
			}
		}
		return false;
	}

	private static Class<?>[] copyWithoutFirst(Class<?>[] values) {
		if (values.length == 1) {
			return EMPTY_CLASS_ARRAY;
		}
		var result = new Class<?>[values.length - 1];
		System.arraycopy(values, 1, result, 0, result.length);
		return result;
	}

	private static boolean isKotlinType(Class<?> clazz) {
		return kotlinMetadata != null //
				&& clazz.getDeclaredAnnotation(kotlinMetadata) != null;
	}

	public static Class<?> getKotlinSuspendingFunctionReturnType(Method method) {
		requireKotlinReflect(method);
		return KotlinSuspendingFunctionUtils.getReturnType(method);
	}

	public static Type getKotlinSuspendingFunctionGenericReturnType(Method method) {
		requireKotlinReflect(method);
		return KotlinSuspendingFunctionUtils.getGenericReturnType(method);
	}

	public static Parameter[] getKotlinSuspendingFunctionParameters(Method method) {
		requireKotlinReflect(method);
		return KotlinSuspendingFunctionUtils.getParameters(method);
	}

	public static Class<?>[] getKotlinSuspendingFunctionParameterTypes(Method method) {
		requireKotlinReflect(method);
		return KotlinSuspendingFunctionUtils.getParameterTypes(method);
	}

	public static @Nullable Object invokeKotlinSuspendingFunction(Method method, @Nullable Object target,
			@Nullable Object[] args) {
		requireKotlinReflect(method);
		requireKotlinxCoroutines(method);
		return KotlinSuspendingFunctionUtils.invoke(method, target, args);
	}

	private static void requireKotlinReflect(Method method) {
		requireDependency(method, kotlinReflectPresent, "org.jetbrains.kotlin:kotlin-reflect");
	}

	private static void requireKotlinxCoroutines(Method method) {
		requireDependency(method, kotlinxCoroutinesPresent, "org.jetbrains.kotlinx:kotlinx-coroutines-core");
	}

	private static void requireDependency(Method method, boolean condition, String dependencyNotation) {
		Preconditions.condition(condition,
			() -> ("Kotlin suspending function [%s] requires %s to be on the classpath or module path. "
					+ "Please add a corresponding dependency.").formatted(method.toGenericString(),
						dependencyNotation));
	}

	private KotlinReflectionUtils() {
	}

}
